/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.lighting.shortlink.project.service.impl;


import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lighting.shortlink.project.common.convention.exception.ClientException;
import com.lighting.shortlink.project.common.convention.exception.ServiceException;
import com.lighting.shortlink.project.common.enums.VailDateTypeEnum;
import com.lighting.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.lighting.shortlink.project.dao.entity.*;
import com.lighting.shortlink.project.dao.mapper.*;
import com.lighting.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.lighting.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.lighting.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.lighting.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.lighting.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.lighting.shortlink.project.dto.resp.*;
import com.lighting.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.lighting.shortlink.project.service.LinkStatsTodayService;
import com.lighting.shortlink.project.service.ShortLinkService;
import com.lighting.shortlink.project.toolkit.HashUtil;
import com.lighting.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static com.lighting.shortlink.project.common.constant.RedisKeyConstant.*;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final LinkStatsTodayService linkStatsTodayService;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;//域名白名单

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;//短链接的域名

    /**
     * 创建短链接（布隆过滤器）
     *
     * @param requestParam 创建短链接请求参数
     * @return 短链接创建信息
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 验证白名单
        verificationWhitelist(requestParam.getOriginUrl());
        // 生成短链接后缀
        String shortLinkSuffix = generateSuffix(requestParam);
        // 构建完整的短链接URL
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        // 构建短链接数据对象
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        // 构建短链接跳转数据对象
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            // 插入短链接数据到数据库
            baseMapper.insert(shortLinkDO);
            // 插入短链接和分组映射数据 到数据库
            shortLinkGotoMapper.insert(linkGotoDO);
        } catch (DuplicateKeyException ex) {
            // 如果发生唯一键冲突，检查布隆过滤器中是否存在该短链接
            if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl)) {
                shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);//不存在则添加
            }
            // 抛出服务异常
            throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
        }
        // 将短链接原始 URL设置到Redis缓存中
        stringRedisTemplate.opsForValue().set(
//                short-link:goto:%s
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),//key
                requestParam.getOriginUrl(),//value
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),//timeout
                TimeUnit.MILLISECONDS//timeUnit
        );
        // 将短链接URL添加到布隆过滤器中
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        // 返回响应对象
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())//短链接
                .originUrl(requestParam.getOriginUrl())//原始链接
                .gid(requestParam.getGid())//分组 id
                .build();
    }

    /**
     * 根据分布式锁创建短链接
     *
     * @param requestParam 创建短链接请求参数
     * @return 短链接创建信息
     */
    @Override
    public ShortLinkCreateRespDTO createShortLinkByLock(ShortLinkCreateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());
        String fullShortUrl;
        // 获取分布式锁
        RLock lock = redissonClient.getLock(SHORT_LINK_CREATE_LOCK_KEY);
        lock.lock();
        try {
            // 生成短链接后缀
            String shortLinkSuffix = generateSuffixByLock(requestParam);
            // 构建完整的短链接URL
            fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                    .append("/")
                    .append(shortLinkSuffix)
                    .toString();
            // 构建短链接数据对象
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(createShortLinkDefaultDomain)
                    .originUrl(requestParam.getOriginUrl())
                    .gid(requestParam.getGid())
                    .createdType(requestParam.getCreatedType())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .describe(requestParam.getDescribe())
                    .shortUri(shortLinkSuffix)
                    .enableStatus(0)
                    .totalPv(0)
                    .totalUv(0)
                    .totalUip(0)
                    .delTime(0L)
                    .fullShortUrl(fullShortUrl)
                    .favicon(getFavicon(requestParam.getOriginUrl()))//发送网络请求获取网站图标（很慢）
                    .build();
            // 构建短链接跳转数据对象
            ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(requestParam.getGid())
                    .build();
            try {
                // 插入短链接数据到数据库
                baseMapper.insert(shortLinkDO);
                // 插入短链接跳转数据到数据库
                shortLinkGotoMapper.insert(linkGotoDO);
            } catch (DuplicateKeyException ex) {
                // 如果发生唯一键冲突，抛出服务异常
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }
            // 将短链接原始URL设置到Redis缓存中
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    requestParam.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
        } finally {
            // 释放分布式锁
            lock.unlock();
        }
        // 返回响应对象
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    /**
     * 批量创建短链接
     *
     * @param requestParam 批量创建短链接请求参数
     * @return 批量创建短链接返回参数
     */
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        // 获取批量创建短链接的请求参数中的原始URL列表和描述列表
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        // 初始化一个空的ShortLinkBaseInfoRespDTO列表，用于存储创建成功的短链接信息
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        // 遍历原始URL列表
        for (int i = 0; i < originUrls.size(); i++) {
            // 提取公共信息：将请求参数对象的通用属性 有效时间、分组 赋值到 idShortLinkCreateReqDTO 类型的对象
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                // 尝试调用createShortLink方法创建短链接
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                // 构建一个ShortLinkBaseInfoRespDTO对象，包含短链接的完整URL、原始URL和描述
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                // 将ShortLinkBaseInfoRespDTO对象添加到结果列表中
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                // 如果在创建过程中发生异常，记录错误日志，但不中断整个批量创建过程
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        // 创建一个ShortLinkBatchCreateRespDTO对象，包含成功创建的短链接总数和短链接信息列表
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    /**
     * 修改短链接
     *
     * @param requestParam 修改短链接请求参数
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());//验证白名单
//        构建查询条件，查找与短链接ID、短链接URL、未删除标志和启用状态匹配的短链接记录
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
        if (hasShortLinkDO == null) { // 如果没有找到匹配的短链接记录，抛出客户端异常
            throw new ClientException("短链接记录不存在");
        }
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) { // 没改分组的情况
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    // 有效期分类讨论：永久则无 ValidDate，自定义有效期才又 ValidDate
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()),//如果有效期为永久,则更新
                            ShortLinkDO::getValidDate,//要更新的字段：有效期列 colum
                            null);//永久有效期无需 过期时间
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())// 短链接的域名
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())//描述
                    .validDateType(requestParam.getValidDateType())//有效期类型
                    .validDate(requestParam.getValidDate())//有效时间
                    .build();
            baseMapper.update(shortLinkDO, updateWrapper);
        }
        //改了分组的情况
        else {
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();// 获取写锁，写锁 与 读、写锁 互斥
            rLock.lock();//上锁
            try {
//                匹配条件
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);//BaseDO 的属性不能直接通过 builder 建立
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);//删除原始分组的短链接
//                创建新分组的短链接
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);//把更改后的短链接插入数据库
//                更新每日监控信息数据
                LambdaQueryWrapper<LinkStatsTodayDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkStatsTodayDO.class)
                        .eq(LinkStatsTodayDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkStatsTodayDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkStatsTodayDO::getDelFlag, 0);
                List<LinkStatsTodayDO> linkStatsTodayDOList = linkStatsTodayMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    // 先批量删除
                    linkStatsTodayMapper.deleteBatchIds(linkStatsTodayDOList.stream()
                            .map(LinkStatsTodayDO::getId)
                            .toList()
                    );
                    //改了 gid 后再批量增加
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid()));
                    linkStatsTodayService.saveBatch(linkStatsTodayDOList);
                }
//                更新路由表
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);
//                更新基础的短链接访问监控信息
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);
//                更新访问位置监控信息
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);
//                更新访问系统监控信息
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);
//              更新访问设备浏览器监控信息
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);
//              更新访问设备设备监控信息
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);
//              更新访问设备网络监控信息
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);
//                更新访问日志信息
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
            } finally {
                rLock.unlock();
            }
        }
        // 有修改短链接的 有效期类型或时间
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType()) // 有效类型不相等
                || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())) { // 有效日期不相等
//           删除(短链接，长链接) 快速跳转缓存
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            //数据库中的短链接过期了，但新的短链接没过期
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date())) {
                if (Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType())//新的是永久有效类型
                        || requestParam.getValidDate().after(new Date())) {//新的过期时间还有效
//                  把 redis 缓存的空值删掉
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        return null;
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        return null;
    }

    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {

    }

    @Override
    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO shortLinkStatsRecord) {

    }

    /**
     * 生成短链接后缀(布隆过滤器)
     *
     * @param requestParam 请求参数
     * @return 短链接后缀
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0; // 自定义生成计数器
        String shorUri; // 短链接后缀
        while (true) {
            if (customGenerateCount > 10) { // 如果自定义生成次数超过10次
                throw new ServiceException("短链接频繁生成，请稍后再试"); // 抛出服务异常
            }
            String originUrl = requestParam.getOriginUrl(); // 获取原始链接
            originUrl += UUID.randomUUID().toString(); // 在原始链接后拼接一个UUID
            // 使用哈希算法将生成的短链接后缀转换为Base62编码
            shorUri = HashUtil.hashToBase62(originUrl);
            // 判断短链接是否存在（容忍误判性：布隆不存在 一定不存在，存在 有可能也不存在）
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shorUri)) {
                break; // 如果布隆过滤器中不存在该短链接，则退出循环
            }
            customGenerateCount++; // 自定义生成计数器加1
        }
        return shorUri; // 返回短链接后缀
    }

    /**
     * 生成短链接后缀(查询数据库)
     *
     * @param requestParam 请求参数
     * @return 短链接后缀
     */
    private String generateSuffixByLock(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0; //  计数器
        String shortUri; // 短链接后缀
        while (true) {
            if (customGenerateCount > 10) { // 如果 生成次数超过10次
                throw new ServiceException("短链接频繁生成，请稍后再试"); // 抛出服务异常
            }
            String originUrl = requestParam.getOriginUrl(); // 获取原始链接
            originUrl += UUID.randomUUID().toString(); // 在原始链接后拼接一个UUID
            // 使用哈希算法将生成的短链接后缀转换为 6位的 Base62编码（五百亿个：56800235584）
            shortUri = HashUtil.hashToBase62(originUrl);
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid()) // 筛选短链接ID匹配的记录
                    .eq(ShortLinkDO::getFullShortUrl, createShortLinkDefaultDomain + "/" + shortUri) // 筛选短链接URL匹配的记录
                    .eq(ShortLinkDO::getDelFlag, 0); // 筛选未删除的记录
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper); // 查询短链接记录
            if (shortLinkDO == null) { // 如果没有重复，表示当前 shortUri 可用
                break; // 退出循环
            }
            customGenerateCount++; // 计数器加1
        }
        return shortUri; // 返回短链接后缀
    }

    /**
     * 获取网站的图标
     *
     * @param url 网站的 url
     * @return 网站图标的地址
     */
    @SneakyThrows
    private String getFavicon(String url) {
        // 创建一个 URL对象，表示要请求的网页地址
        URL targetUrl = new URL(url);
        // 打开一个到目标URL的HTTP连接
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        // 设置请求方法为GET
        connection.setRequestMethod("GET");
        // 建立连接
        connection.connect();
        // 获取响应码
        int responseCode = connection.getResponseCode();
        // 如果响应码是HTTP_OK（200），则继续处理
        if (HttpURLConnection.HTTP_OK == responseCode) {
            // 使用Jsoup连接到网页，并获取整个文档
            Document document = Jsoup.connect(url).get();
            // 选择所有具有rel属性包含"icon"或"shortcut icon"的link标签
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            // 如果找到了favicon的link标签，则获取其href属性的绝对路径
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        // 如果没有找到favicon或者响应码不是HTTP_OK，则返回null
        return null;
    }

    /**
     * 验证白名单功能
     *
     * @param originUrl 跳转链接的原始URL
     */
    private void verificationWhitelist(String originUrl) {
        // 获取白名单配置的启用状态
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        // 如果白名单未启用或未设置，则直接返回
        if (enable == null || !enable) {
            return;
        }
        // 提取跳转链接的域名
        String domain = LinkUtil.extractDomain(originUrl);
        // 如果域名为空，则抛出客户端异常
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        // 获取白名单中的域名列表
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();

        // 如果提取的域名不在白名单中，则抛出客户端异常
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}